package io.noties.markwon.base

import org.commonmark.node.Node
import java.lang.NullPointerException
import java.util.ArrayList
import java.util.Collections
import java.util.HashMap

/**
 * @since 3.0.0
 */
fun interface SpanFactory {
    fun getSpans(
        configuration: MarkwonConfiguration,
        props: RenderProps
    ): Any
}


/**
 * Class that controls what spans are used for certain Nodes.
 *
 * @see io.noties.markwon.SpanFactory
 *
 * @since 3.0.0
 */
interface MarkwonSpansFactory {
    /**
     * Returns registered [io.noties.markwon.base.SpanFactory] or `null` if a factory for this node type
     * is not registered. There is [.require] method that will throw an exception
     * if required [io.noties.markwon.base.SpanFactory] is not registered, thus making return type `non-null`
     *
     * @param node type of the node
     * @return registered [io.noties.markwon.base.SpanFactory] or null if it\'s not registered
     * @see .require
     */
    operator fun <N : Node> get(node: Class<N>): SpanFactory?
    fun <N : Node> require(node: Class<N>): SpanFactory
    interface Builder {

        fun <N : Node> setFactory(node: Class<N>, factory: SpanFactory?): Builder

        fun <N : Node> setFactory(node: Class<N>, factory: (configuration: MarkwonConfiguration,
                                                            props: RenderProps)->Any): Builder

        /**
         * Helper method to add a [io.noties.markwon.SpanFactory] for a Node. This method will merge existing
         * [io.noties.markwon.SpanFactory] with the specified one.
         *
         * @since 3.0.1
         */
        @Deprecated(
            """4.2.2 consider using {@link #appendFactory(Class, io.noties.markwon.ext.SpanFactory)} or
          {@link #prependFactory(Class, io.noties.markwon.ext.SpanFactory)} methods for more explicit factory ordering.
          `addFactory` behaved like {@link #prependFactory(Class, io.noties.markwon.SpanFactory)}, so
          this method call can be replaced with it"""
        )
        fun <N : Node> addFactory(node: Class<N>, factory: SpanFactory): Builder

        /**
         * Append a factory to existing one (or make the first one for specified node). Specified factory
         * will be called **after** original (if present) factory. Can be used to
         * *change* behavior or original span factory.
         *
         * @since 4.2.2
         */
        fun <N : Node> appendFactory(node: Class<N>, factory: SpanFactory): Builder

        /**
         * Prepend a factory to existing one (or make the first one for specified node). Specified factory
         * will be called <string>before</string> original (if present) factory.
         *
         * @since 4.2.2
         */
        fun <N : Node> prependFactory(node: Class<N>, factory: SpanFactory): Builder

        /**
         * Can be useful when *enhancing* an already defined SpanFactory with another one.
         */
        fun <N : Node> getFactory(node: Class<N>): SpanFactory?

        /**
         * To obtain current [io.noties.markwon.SpanFactory] associated with specified node. Can be used
         * when SpanFactory must be present for node. If it\'s not added/registered a runtime
         * exception will be thrown
         *
         * @see .getFactory
         * @since 3.0.1
         */
        fun <N : Node> requireFactory(node: Class<N>): SpanFactory

        fun build(): MarkwonSpansFactory
    }
}

/**
 * @since 3.0.0
 */
internal class MarkwonSpansFactoryImpl(private val factories: Map<Class<out Node>, SpanFactory>) :
    MarkwonSpansFactory {
    override fun <N : Node> get(node: Class<N>): SpanFactory? {
        return factories[node]
    }

    override fun <N : Node> require(node: Class<N>): SpanFactory {
        return get(node) ?: throw NullPointerException(node.name)
    }

    internal class BuilderImpl : MarkwonSpansFactory.Builder {
        private val factories: MutableMap<Class<out Node>, SpanFactory> = HashMap(3)
        override fun <N : Node> setFactory(
            node: Class<N>,
            factory: SpanFactory?
        ): MarkwonSpansFactory.Builder {
            if (factory == null) {
                factories.remove(node)
            } else {
                factories[node] = factory
            }
            return this
        }

        override fun <N : Node> setFactory(
            node: Class<N>,
            factory: (configuration: MarkwonConfiguration, props: RenderProps) -> Any
        ): MarkwonSpansFactory.Builder {
            setFactory(node,
                SpanFactory { configuration, props -> factory(configuration, props) })
            return this
        }


        @Deprecated("")
        override fun <N : Node> addFactory(
            node: Class<N>,
            factory: SpanFactory
        ): MarkwonSpansFactory.Builder {
            return prependFactory(node, factory)
        }

        override fun <N : Node> appendFactory(
            node: Class<N>,
            factory: SpanFactory
        ): MarkwonSpansFactory.Builder {
            val existing = factories[node]
            if (existing == null) {
                factories[node] = factory
            } else {
                if (existing is CompositeSpanFactory) {
                    existing.factories.add(0, factory)
                } else {
                    val compositeSpanFactory = CompositeSpanFactory(factory, existing)
                    factories[node] = compositeSpanFactory
                }
            }
            return this
        }

        override fun <N : Node> prependFactory(
            node: Class<N>,
            factory: SpanFactory
        ): MarkwonSpansFactory.Builder {
            // if there is no factory registered for this node -> just add it
            val existing = factories[node]
            if (existing == null) {
                factories[node] = factory
            } else {
                // existing span factory can be of CompositeSpanFactory at this point -> append to it
                if (existing is CompositeSpanFactory) {
                    existing.factories.add(factory)
                } else {
                    // if it's not composite at this point -> make it
                    val compositeSpanFactory = CompositeSpanFactory(existing, factory)
                    factories[node] = compositeSpanFactory
                }
            }
            return this
        }

        override fun <N : Node> getFactory(node: Class<N>): SpanFactory? {
            return factories[node]
        }

        override fun <N : Node> requireFactory(node: Class<N>): SpanFactory {
            return getFactory(node) ?: throw NullPointerException(node.name)
        }

        override fun build(): MarkwonSpansFactory {
            return MarkwonSpansFactoryImpl(Collections.unmodifiableMap(factories))
        }
    }

    internal class CompositeSpanFactory(first: SpanFactory, second: SpanFactory) : SpanFactory {
        val factories: MutableList<SpanFactory>

        init {
            factories = ArrayList(3)
            factories.add(first)
            factories.add(second)
        }

        override fun getSpans(configuration: MarkwonConfiguration, props: RenderProps): Any {
            // please note that we do not check it factory itself returns an array of spans,
            // as this behaviour is supported now (previously we supported only a single-level array)
            val length = factories.size
            val out = arrayOfNulls<Any>(length)
            for (i in 0 until length) {
                out[i] = factories[i].getSpans(configuration, props)
            }
            return out
        }
    }
}